/*-
 * RS232Communicator.java
 *
 * Copyright (c) 2007, Stein Gunnar Bakkeby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Stein Gunnar Bakkeby nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.attention.rs232;

import static com.googlecode.attention.ComLogStatements.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.googlecode.attention.Communicator;
import com.googlecode.attention.CommunicatorStatus;
import com.googlecode.attention.util.Settings;

import gnu.io.*;


/**
 * The RS232 communicator, used to manage communication over a serial port.
 *
 * @author Stein Gunnar Bakkeby
 */
public class RS232Communicator implements Communicator,
                                          SerialPortEventListener {
  
  /* Local variables */

  /** The serial port's name in the operating system. */
  private String portName;
  /** Serial communication data rate. The data rate the port will use. */
  private int baudrate;
  /** Serial communication data bits. Should always be set to 8 for AT commands. */
  private int databits;
  /** Serial communication stop bits. Should always be set to 1 for AT commands. */
  private int stopbits;
  /** Serial communication parity bit. Should always be set to 0 for AT commands. */
  private int parity;
  
  /** The name that will be published as the owner for the serial port. */
  private String appName;
  private int timeout;
  private int capacity;
  //private String[] stopOn;
  
  private InputStream reader; // Data from the serial port.
  private BufferedOutputStream writer;
  private BlockingQueue<String> queue;
  
  private SerialPort serialPort;
  private CommunicatorStatus status;

  /**
   * Constructs a LinuxSerialManager with a given port number and data rate.
   *
   * @param settings ResourceBundle with the given settings.
   * @throws IOException When error retrieving the streams from the serial port
   */
  public RS232Communicator(final Settings settings) throws IOException {
    setStatus(CommunicatorStatus.INITIALISING);
    portName = settings.getString("port_name");
    baudrate = settings.getInteger("baudrate");
    databits = settings.getInteger("databits");
    stopbits = settings.getInteger("stopbits");
    parity   = settings.getInteger("parity");
    appName  = settings.getString("app_name");
    timeout  = settings.getInteger("timeout");
    capacity = settings.getInteger("capacity");
    //stopOn   = settings.getList("stop_on_");
    serialPort = getSerialPort(portName);
    if (serialPort == null) throw new IOException("No serial port found");
    reader = serialPort.getInputStream();
    writer = new BufferedOutputStream(serialPort.getOutputStream());
    try {
      serialPort.addEventListener(this);
    } catch (TooManyListenersException e) {
      COM_DEBUG.log(e);
      throw new IOException(e.getMessage());
    }
    /* Notifies this class when there are data available at RS232. */
    serialPort.notifyOnDataAvailable(true);
    /* Notifies this class when a message has been sent. */
    serialPort.notifyOnOutputEmpty(true);
    queue = new ArrayBlockingQueue<String>(capacity);
    setStatus(CommunicatorStatus.WAITING);
    final Reader serialReader = new Reader(queue, reader);
    new Thread(serialReader).start();
  }

  /**
   * Returns the port in use by this object.
   *
   * @return portNumber
   */
  public String getPortNumber() {
    return portName;
  }

  /**
   * Returns the data rate at the port.
   *
   * @return The data rate
   */
  public int getDataRate() {
    return baudrate;
  }

  /**
   * Returns the SerialPort used by this SerialManager.
   *
   * @return SerialPort
   */
  public SerialPort getSerialPort() {
    return serialPort;
  }
  
  /**
   * Sends an AT command.
   */
  public void sendAT(String command) {
    setStatus(CommunicatorStatus.SENDING);
    try {
      COM_DEBUG.log("Sending AT command: \"" + command.replace("\n", "\\n").replace("\r", "\\r") + '\"');
      final Pattern pattern = Pattern.compile("([^\r]+)[\r][\n](.+)");
      final Matcher matcher = pattern.matcher(command);
      if (matcher.find()) {
        writer.write((matcher.group(1) + "\r").getBytes());
        writer.flush();
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        writer.write((matcher.group(2)).getBytes());
        writer.flush();
      } else {
        writer.write((command + "\r\n").getBytes());
        writer.flush();  
      }
    } catch (IOException e) {
      COM_DEBUG.log(e);
    }
    setStatus(CommunicatorStatus.WAITING);
  }

  /**
   * Waits to receive and return the response to a sent AT command.
   * 
   * @return The response, or null if timeout
   */
  public String getResponse(final long timeout, final TimeUnit unit) {
    try {
      return queue.poll(timeout, unit);
    } catch (InterruptedException e) {
      return null;
    }
  }

  /**
   * Tries to open a serial port with a given name.
   *
   * @param portName The name of the serial port, f.ex. "COM1" or "/dev/ttyS0"
   * @return serialPort If successful, otherwise null is returned
   */
  public SerialPort getSerialPort(final String portName) {
    SerialPort sPort = null;
    try {
      sPort = tryToOpenSerialPort(CommPortIdentifier.getPortIdentifier(portName));
    } catch (NoSuchPortException nspe) {
      COM_003.log(nspe, portName);
    }
    return sPort;
  }
  
  /**
   * Tries to search for an available serial port.
   * 
   * @return A serial port or null if none was found.
   */
  @SuppressWarnings("unchecked")
  public SerialPort searchForSerialPort() {
    CommPortIdentifier portId;
    final Enumeration portList = CommPortIdentifier.getPortIdentifiers();
    
    while (portList.hasMoreElements()) {
      portId = (CommPortIdentifier) portList.nextElement();
      if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
        final SerialPort port = tryToOpenSerialPort(portId);
        // TODO need to validate here by sending an "AT" and expecting an "OK" in return
        // not doing it here and now as I only have one serial port
        if (port != null) return port;
      }
    }
    return null;
  }
  
  private SerialPort tryToOpenSerialPort(final CommPortIdentifier comm) {
    try {
      final SerialPort serialPort = (SerialPort) comm.open(appName, timeout);
      serialPort.setSerialPortParams(baudrate, databits, stopbits, parity);
      return serialPort;
    } catch (PortInUseException e) {
      COM_001.log(e, comm.getName());
    } catch (UnsupportedCommOperationException e) {
      COM_DEBUG.log(e);
    }
    return null;
  }

  /**
   * This method is listening for events generated by the SerialPortobject.
   * If no terminator is being used it generates a CommunicationEvent object
   * when data arrives. If a terminator is set it generates a Communication-
   * Event object when terminated data arrives. All listeners will receive
   * these events.
   *
   * @param event a event generated by the SerialPort
   */
  public void serialEvent(final SerialPortEvent event) {
    switch (event.getEventType()) {
    case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
      /* Output buffer empty and the class is ready to send again. */
      COM_DEBUG.log("Output buffer is empty and the class is ready to send again.");
      //setStatus(CommunicatorStatus.WAITING);
      break;

    /* If the serial event is of type DATA_AVAILABLE. */
    case SerialPortEvent.DATA_AVAILABLE: //Data has arrived at the SerialPort
      COM_DEBUG.log("Notification: Reading available data on serial port");
//      setStatus(CommunicatorStatus.READING);
//      try {
//        int count = 1;
//        int available = reader.available();
//        final StringBuilder sb = new StringBuilder(available); // 8x more chars than available
//        while (available > 0) { // Reads data from the input stream.
//          final byte[] buffer = new byte[available];
//          reader.read(buffer, 0, available);
//          sb.append(new String(buffer));
//          try {
//            Thread.sleep(500*count); // minimum 15
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
//          count += 2;
//          available = reader.available();
//        }
//        final String offer = sb.toString();
////        int index = offer.indexOf("\n\r");
////        System.out.println("offer.length: " + offer.length());
////        while (index >= 0) {
////          System.out.println("offer.index: " + index);
////          index = offer.indexOf("\n\r", index + 4);
////        }
//        
//        final boolean offerResult = queue.offer(offer);
//        if (offerResult) {
//          COM_004.log(offer);
//        } else {
//          COM_DEBUG.log("RS232Communicator.serialEvent(..): case SerialPortEvent.DATA_AVALABLE: Queue is full?");
//        }
//      } catch (IOException e) {
//        COM_DEBUG.log(e);
//      }
      break;

    default:
      COM_DEBUG.log("RS232Communicator.serialEvent(SerialPort): Default switch case used when event type is " + event.getEventType());
      break;
    }
//    setStatus(CommunicatorStatus.WAITING);
  }
  
  private class Reader implements Runnable {
    
    private BlockingQueue<String> queue;
    private InputStream reader;

    Reader(final BlockingQueue<String> queue, final InputStream reader) {
      this.queue = queue;
      this.reader = reader;
    }
    
    public void run() {
      do {
        try {
          int count = 1;
          int available;
          setStatus(CommunicatorStatus.WAITING);
          do {
            try {
              Thread.sleep(200);
            } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
            }
            available = reader.available();
          } while (available == 0);
          COM_DEBUG.log("Reading available data on serial port");
          setStatus(CommunicatorStatus.READING);
          final StringBuilder sb = new StringBuilder(available); // 8x more chars than available
          while (available > 0) { // Reads data from the input stream.
            final byte[] buffer = new byte[available];
            reader.read(buffer, 0, available);
            sb.append(new String(buffer));
            try {
              Thread.sleep(500*count); // minimum 15
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            count += 2;
            available = reader.available();
          }
          final String offer = sb.toString();
          
          final boolean offerResult = queue.offer(offer);
          if (offerResult) {
            COM_004.log(offer);
          } else {
            COM_DEBUG.log("RS232Communicator.serialEvent(..): case SerialPortEvent.DATA_AVALABLE: Queue is full?");
          }
        } catch (IOException e) {
          COM_DEBUG.log(e);
        }
      } while (true); // TODO terminator or similar
    }
  }

  @Override
  public CommunicatorStatus getStatus() {
    return status;
  }
  
  protected void setStatus(final CommunicatorStatus status) {
    this.status = status;
    COM_DEBUG.log("Communicator status changed to: " + status);
  }
}